#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Datetime: 2020/12/18 14:48
# @Author  : thinkive_cfy_ide_3
# @Site    : 
# @File    : holding_based_allocation.py
# @Software: PyCharm 

"""
脚本说明
"""

import pandas as pd
from quant_researcher.quant.datasource_fetch.stock_api.stock_info import \
    get_stock_industry_classification
from quant_researcher.quant.datasource_fetch.factor_api import factor_exposure_related
# from quant_researcher.quant.datasource_fetch.fund_api.fund_holding_related import \
#     get_holding_report_info
from quant_researcher.quant.project_tool import time_tool
from quant_researcher.quant.datasource_fetch.fund_api.fund_holding_related import get_holding_report_info
from quant_researcher.quant.datasource_fetch.index_api.index_constant import SW_SNAME_LIST
# from quant_researcher.quant.factors.factor_database.mutual_fund.fnd_nav_guess import \
#     common_functions
from quant_researcher.quant.project_tool.logger.my_logger import LOG
from quant_researcher.quant.project_tool.time_tool import date_shifter
from quant_researcher.quant.datasource_fetch.stock_api import stock_price_related


def get_holding_industry_allocation(end_date, stock_holding, industry_type='SW', stock_detail=True, **kwargs):
    """

    :param end_date: 行业分类日期
    :param pd.DataFrame stock_holding: 股票组合的详细权重信息，格式如下
    security_code：证券代码
    security_weight：证券权重
     ---------------+-----------------------+
    | security_code | security_weight |
     ---------------+-----------------------+
         000333    |         0.0968        |
         000423    |         0.0192        |
         000568    |         0.0879        |
         000596    |         0.0816        |
         000651    |         0.0971        |
     ---------------+-----------------------+
    :param str industry_type: 目前支持SW
    :param bool stock_detail: 是否返回个股详细信息
    :return: 返回股票持仓的行业配置，未配置的行业返回0

    """
    conn = kwargs.pop('conn', None)
    if industry_type=='SW':
        induclsstd_code = 'sw2014'
    else:
        raise ValueError('目前仅支持申万分类')

    industry_stock = get_stock_industry_classification(induclsstd_code=induclsstd_code, end_date=end_date, conn=conn)[[
        'stock_code', 'indu_name']]
    unique_ind_code = industry_stock.indu_name.unique().tolist()
    industry_stock = industry_stock.drop_duplicates(subset=['stock_code'])
    industry_stock.columns = ['security_code','indu_name']
    industry_df = stock_holding.merge(industry_stock, how='left', on='security_code')
    if stock_detail:
        industry_df['industry_weight'] = industry_df.groupby('indu_name')['security_weight'].transform('sum')
        industry_df['industry_weight'] = industry_df['industry_weight'].fillna(0)
    else:
        industry_df = industry_df.groupby('indu_name')['security_weight'].sum()
        industry_df = industry_df.reindex(unique_ind_code).fillna(0).reset_index()
        industry_df.columns = ['industry','industry_weight']
    return industry_df


def get_holding_industry_weight_and_return(begin_date, end_date, holding_begin, holding_end,
                                           industry_type='SW', **kwargs):
    """

    :param begin_date:期初持仓日期
    :param end_date:期末持仓日期
    :param pd.DataFrame holding_begin: 开始时点股票组合的详细权重信息，格式如下
    security_code：证券代码
    security_weight：证券权重
        ---------------+-----------------------+
        security_code | security_weight |
        ---------------+-----------------------+
            000333    |         0.0968        |
            000423    |         0.0192        |
            000568    |         0.0879        |
            000596    |         0.0816        |
            000651    |         0.0971        |
        ---------------+-----------------------+
    :param pd.DataFrame holding_end: 结束时点股票组合的详细权重信息，格式如下
    security_code：证券代码
    security_weight：证券权重
        ---------------+-----------------------+
        security_code | security_weight |
        ---------------+-----------------------+
            000333    |         0.0968        |
            000423    |         0.0192        |
            000568    |         0.0879        |
            000596    |         0.0816        |
            000651    |         0.0971        |
        ---------------+-----------------------+
    :param industry_type:
    :return:
    """
    conn_base = kwargs.pop('conn_base', None)
    conn_stock = kwargs.pop('conn_stock', None)
    # 组合内股票在该区间内的平均持仓权重
    holding_df = holding_begin.merge(holding_end, on='security_code', how='outer',
                                     suffixes=['_begin', '_end'])
    holding_avg = holding_df.set_index('security_code').fillna(0).mean(axis=1)
    holding_avg = holding_avg.reset_index()
    holding_avg.columns = ['security_code', 'security_weight']

    # 组合平均行业权重
    ind_weight_avg_df = get_holding_industry_allocation(end_date, holding_avg,
                                                        industry_type=industry_type, conn=conn_base)
    ind_weight_avg_df = ind_weight_avg_df.dropna(subset=['indu_name'])  # 为空的为打新持有的，目前还没有行业分类
    ind_weight_avg = ind_weight_avg_df[['indu_name', 'industry_weight']].drop_duplicates()
    ind_weight_avg = ind_weight_avg.set_index('indu_name')['industry_weight'].reindex(
        SW_SNAME_LIST).fillna(0)

    # 组合股票收益率
    stock_list = pd.concat([holding_begin, holding_end]).security_code.unique().tolist()
    hq_begin = date_shifter(begin_date, step='days', how_many=1)
    stock_ret = stock_price_related.get_stock_total_return(hq_begin, end_date, stock_code=stock_list,
                                                           conn=conn_stock)
    stock_ret.columns = ['security_code', 'security_ret']

    # 股票在组合行业的相对权重
    ind_weight_avg_df['security_relative_weight'] = ind_weight_avg_df['security_weight'] / \
                                                    ind_weight_avg_df['industry_weight']

    # 组合在每个行业的收益率
    ind_security_df = ind_weight_avg_df.merge(stock_ret, on='security_code', how='left')
    ind_security_df['weighted_security_ret'] = ind_security_df['security_relative_weight'] * \
                                               ind_security_df['security_ret']
    ind_ret = ind_security_df.groupby('indu_name')['weighted_security_ret'].sum().reindex(
        SW_SNAME_LIST).fillna(0)

    return ind_weight_avg, ind_ret


def get_holding_asset_allocation(portfolio_holding, sum_to_1=False):
    """
    统计各大类资产在组合中的占比。

    :param pd.DataFrame portfolio_holding: 组合的持仓信息，格式如下
    asset_type：大类资产类型，函数将基于本字段的不同类型进行统计
    security_code：证券代码；
    security_weight：证券权重
    ------------+---------------+-----------------------+
        | asset_type | security_code | security_weight |
      ------------+---------------+-----------------------+
         cash    |      cash     |         0.1121        |
        stock    |     000333    |         0.0968        |
        stock    |     000423    |         0.0192        |
        stock    |     000568    |         0.0879        |
        stock    |     000596    |         0.0816        |
        stock    |     000651    |         0.0971        |
         bond    |     050203    |         0.0162        |
         bond    |     150313    |         0.0049        |
         bond    |     150415    |         0.0049        |
         bond    |     170205    |         0.0049        |
         bond    |     190304    |         0.0049        |
      ------------+---------------+-----------------------+
    :param sum_to_1: 是否将持仓权重调整为加和为1。默认为False
    :return:
    """
    if sum_to_1:
        portfolio_holding['security_weight'] /= portfolio_holding['security_weight'].sum()
    result = portfolio_holding.groupby('asset_type')['security_weight'].sum()
    result = result.reset_index()
    result.columns = ['asset', 'asset_weight']
    return result


def get_holding_barra_style_factor_allocation(portfolio_holding, end_date):
    """
    获取组合持仓的barra风格因子暴露，计算时默认会排除非股票资产，将股票资产权重之和调整为1。

    :param pd.DataFrame portfolio_holding: 组合的持仓信息，格式如下
    asset_type：大类资产类型，stock: 股票，bond: 债券，cash:现金
    security_code：证券代码；
    security_weight：证券权重
    ------------+---------------+-----------------------+
        | asset_type | security_code | security_weight |
      ------------+---------------+-----------------------+
         cash    |      cash     |         0.1121        |
        stock    |     000333    |         0.0968        |
        stock    |     000423    |         0.0192        |
        stock    |     000568    |         0.0879        |
        stock    |     000596    |         0.0816        |
        stock    |     000651    |         0.0971        |
         bond    |     050203    |         0.0162        |
         bond    |     150313    |         0.0049        |
         bond    |     150415    |         0.0049        |
         bond    |     170205    |         0.0049        |
         bond    |     190304    |         0.0049        |
      ------------+---------------+-----------------------+
    :param end_date: 持仓日期
    :return:
    """
    stock_holding = portfolio_holding[portfolio_holding.asset_type=='stock']
    stock_holding['security_weight'] /= stock_holding['security_weight'].sum()
    stock_pool = stock_holding.security_code.unique().tolist()
    # stock_style_exposure = get_barra_factor_exposure(stock_pool, end_date)
    stock_style_exposure = factor_exposure_related.get_barra_factor_exposure(stock_pool,
                                                                             end_date=end_date,
                                                                             only_latest=True,
                                                                             factor_type='style')
    stock_style_exposure = stock_style_exposure.rename(columns={'stock_code': 'stockcode'})
    stock_style_exposure = stock_style_exposure.drop(columns=['end_date', 'country'])
    stock_style_exposure = stock_style_exposure.set_index('stockcode').stack().rename('exposure').reset_index()
    stock_style_exposure.columns = ['security_code', 'style', 'exposure']
    stock_style_exposure = stock_style_exposure.merge(stock_holding, on='security_code',how='right')
    stock_style_exposure['exposure'] = stock_style_exposure['exposure'].fillna(0)
    stock_style_exposure = stock_style_exposure.eval('weighted_exposure=exposure*security_weight')
    result = stock_style_exposure.groupby('style')['weighted_exposure'].sum()
    result = result.reset_index()
    result.columns=['style','style_exposure']
    return result.reset_index()


def get_holding_drift(portfolio_holding, benchmark_holding, drift_type):
    """
    基于持仓的偏离度计算函数，目前仅计算组合与基准的差值。

    :param portfolio_holding:组合基于持仓的大类资产、行业或者风格的信息
    :param benchmark_holding:基准基于回仓的大类资产、行业或者风格的信息
    :param drift_type: 偏离度类型，目前支持的类型包括：asset，大类资产；industry，行业；style，风格。
    :return:
    """
    p_columns = set(portfolio_holding.columns)
    b_columns = set(benchmark_holding.columns)
    assert p_columns == b_columns, "请确认组合和基准持仓包含相同信息"
    assert drift_type in p_columns, "请确认偏离度计算类型与持仓信息相符"
    all_holding = portfolio_holding.merge(benchmark_holding, on=drift_type, how='left', suffixes=['_p','_b'])

    if drift_type=='asset':
        all_holding['drift'] = all_holding['asset_weight_p'] - all_holding['asset_weight_b']
    elif drift_type=='industry':
        all_holding['drift'] = all_holding['industry_weight_p'] - all_holding['industry_weight_b']
    elif drift_type=='style':
        all_holding['drift'] = all_holding['style_exposure_p'] - all_holding['style_exposure_b']
    else:
        raise ValueError("目前仅支持asset, industry, style 三种类型的偏离度计算")
    result = all_holding[[drift_type,'drift']]
    return result


def get_asset_allocation(fund_list=None, net_or_total='t', only_latest=True, **kwargs):
    """
    得到基金列表中每个基金在计算日期前各自最新的大类资产配置数据
    1 权益投资
        其中：股票
            - 存托凭证 （QDII）
            - 房地产信托 （QDII）
    2 基金投资
    3 固定收益投资
        其中：债券
            资产支持证券
    4 贵金属投资
    5 金融衍生品投资
        其中: 远期
        其中: 期货
        其中: 期权
        其中: 权证
    6 买入返售金融资产
        其中：买断式回购的买入返售金融资产
    7 银行存款和结算备付金合计
    8 其他各项资产

    :param list fund_list: 基金列表， ['001410', '0030955'], 默认为None，即获取所有基金
    :param str net_or_total: 占净资产比例还是占总资产比例，net-净资产，total-总资产
    :param bool only_latest: 是否只获取最新, 默认为True
    :param kwargs: 如果不只是获取最新一期，则需要传入开始时间与结束时间
        - str start_date: 开始时间 "2020-01-01" （结束时间不传则默认为end_date）
        - str end_date: 结束时间 "2020-09-01" (结束时间不传则默认为今天)
        - conn: 数据库连接
    :return:
    """

    end_date = kwargs.pop('end_date', None)
    start_date = kwargs.pop('start_date', None)
    conn = kwargs.pop('conn', None)
    if not only_latest:
        if start_date is None:
            start_date1 = time_tool.get_today(marker='with_n_dash').replace('-', '')
        else:
            start_date1 = start_date
    else:
        start_date1 = start_date
    f_asset_report = get_holding_report_info(fund_code=fund_list, only_latest=only_latest, start_date=start_date1,
                                             end_date=end_date, report_type=[1, 2, 3, 4], conn=conn)

    if f_asset_report.empty:
        LOG.error(f"未找到{fund_list}的资产配置信息")
        return
    f_asset_df = f_asset_report.fillna(0)

    # 权益类资产加上基金投资， 基金投资按道理应该继续分拆
    f_asset_df['stock'] = f_asset_df[f'eqinvt_{net_or_total}appt'] + f_asset_df[f'fndinvt_{net_or_total}appt']
    # 固定收益投资
    f_asset_df['bond'] = f_asset_df[f'fixedin_{net_or_total}appt']
    # 现金类包含买入返售投资和银行存款与结算准备金
    f_asset_df['cash'] = f_asset_df[f'bbsfa_{net_or_total}appt'] + f_asset_df[f'bdlr_{net_or_total}appt']
    if net_or_total == 't':  # 如果是占总资产比，总资产比之和为1
        f_asset_df['other'] = 100 - (f_asset_df['stock'] + f_asset_df['bond'] + f_asset_df['cash'])
    else:  # 如果是占净总资产比，总资产比之和不为1， 其他资产包含贵金属投资，金融衍生品投资，和其他各项投资
        f_asset_df['other'] = f_asset_df[f'pminvt_nappt'] + f_asset_df[f'fdinvt_nappt'] + \
                              f_asset_df[f'others_nappt']

    asset_allocation_df = f_asset_df[['info_pubdate', 'report_date', 'report_type', 'fund_code',
                                      'stock', 'bond', 'cash', 'other']]
    asset_allocation_df[['stock', 'bond', 'cash', 'other']] = asset_allocation_df[
                                                                  ['stock', 'bond', 'cash',
                                                                   'other']] / 100

    return asset_allocation_df


if __name__ == '__main__':
    # 测试 get_asset_allocation, 获取基金资产配置
    # get_asset_allocation(n_or_t='t', only_latest=False, end_date='2020-09-30')
    get_asset_allocation(fund_list=['001410', '003095'], net_or_total='t', only_latest=False,
                         start_date='2020-01-01', end_date='2020-10-30')